/*-------------------------------------------------------------------------
 *
 * pgtz.c
 *	  时区库集成功能
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  src/timezone/pgtz.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <ctype.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>

#include "datatype/timestamp.h"
#include "miscadmin.h"
#include "pgtz.h"
#include "storage/fd.h"
#include "utils/hsearch.h"


/* 当前会话时区（由 TimeZone GUC 控制） */
pg_tz	   *session_timezone = NULL;

/* 当前日志时区（由 log_timezone GUC 控制） */
pg_tz	   *log_timezone = NULL;


static bool fc_scan_directory_ci(const char *fc_dirname,
							  const char *fc_fname, int fc_fnamelen,
							  char *fc_canonname, int fc_canonnamelen);


/*
 * 返回时区数据目录的完整路径名
 */
static const char * fc_pg_TZDIR(void)
{
#ifndef SYSTEMTZDIR
	/* 正常情况下：时区相关内容位于我们的共享目录下 */
	static bool fc_done_tzdir = false;
	static char fc_tzdir[MAXPGPATH];

	if (fc_done_tzdir)
		return fc_tzdir;

	get_share_path(my_exec_path, fc_tzdir);
	strlcpy(fc_tzdir + strlen(fc_tzdir), "/timezone", MAXPGPATH - strlen(fc_tzdir));

	fc_done_tzdir = true;
	return fc_tzdir;
#else
	/* 我们被配置为使用系统的时区数据库 */
	return SYSTEMTZDIR;
#endif
}


/*
 * 给定时区名称，打开时区数据文件。如果成功，则返回文件描述符，失败则返回 -1。
 *
 * 输入名称是大小写不敏感地搜索（我们假设时区数据库不包含大小写等效的名称）。
 *
 * 如果 "canonname" 不是 NULL，则在成功时会将给定名称的规范拼写存储在那里（缓冲区必须大于 TZ_STRLEN_MAX 字节！）。
 */
int pg_open_tzfile(const char *fc_name, char *fc_canonname)
{
	const char *fc_fname;
	char		fc_fullname[MAXPGPATH];
	int			fc_fullnamelen;
	int			fc_orignamelen;

	/* 用 tzdata 目录的基本名称初始化 fullname */
	strlcpy(fc_fullname, fc_pg_TZDIR(), sizeof(fc_fullname));
	fc_orignamelen = fc_fullnamelen = strlen(fc_fullname);

	if (fc_fullnamelen + 1 + strlen(fc_name) >= MAXPGPATH)
		return -1;				/* 不会适配 */

	/*
	 * 如果调用者不需要规范拼写，首先尝试直接打开该名称。如果给定名称
	 * 已经正确大小写，或者文件系统大小写不敏感，则可以预计会成功；如果
	 * 我们不需要区分这些情况，也就不必报告规范拼写。
	 */
	if (fc_canonname == NULL)
	{
		int			fc_result;

		fc_fullname[fc_fullnamelen] = '/';
		/* 上述测试确保这将适配： */
		strcpy(fc_fullname + fc_fullnamelen + 1, fc_name);
		fc_result = open(fc_fullname, O_RDONLY | PG_BINARY, 0);
		if (fc_result >= 0)
			return fc_result;
		/* 如果这不奏效，就按照困难的方法进行 */
		fc_fullname[fc_fullnamelen] = '\0';
	}

	/*
	 * 循环将给定名称拆分为目录级别；对于每个级别，
	 * 使用 scan_directory_ci() 进行搜索。
	 */
	fc_fname = fc_name;
	for (;;)
	{
		const char *fc_slashptr;
		int			fc_fnamelen;

		fc_slashptr = strchr(fc_fname, '/');
		if (fc_slashptr)
			fc_fnamelen = fc_slashptr - fc_fname;
		else
			fc_fnamelen = strlen(fc_fname);
		if (!fc_scan_directory_ci(fc_fullname, fc_fname, fc_fnamelen,
							   fc_fullname + fc_fullnamelen + 1,
							   MAXPGPATH - fc_fullnamelen - 1))
			return -1;
		fc_fullname[fc_fullnamelen++] = '/';
		fc_fullnamelen += strlen(fc_fullname + fc_fullnamelen);
		if (fc_slashptr)
			fc_fname = fc_slashptr + 1;
		else
			break;
	}

	if (fc_canonname)
		strlcpy(fc_canonname, fc_fullname + fc_orignamelen + 1, TZ_STRLEN_MAX + 1);

	return open(fc_fullname, O_RDONLY | PG_BINARY, 0);
}


/*
 * 扫描指定目录以查找与 fname（长度为 fnamelen --- fname 可能未以 null 结束） 的大小写不敏感匹配。如果找到，
 * 则将实际文件名复制到 canonname 并返回 true。
 */
static bool fc_scan_directory_ci(const char *fc_dirname, const char *fc_fname, int fc_fnamelen,
				  char *fc_canonname, int fc_canonnamelen)
{
	bool		fc_found = false;
	DIR		   *fc_dirdesc;
	struct dirent *fc_direntry;

	fc_dirdesc = AllocateDir(fc_dirname);

	while ((fc_direntry = ReadDirExtended(fc_dirdesc, fc_dirname, LOG)) != NULL)
	{
		/*
		 * 忽略 . 和 ..，以及任何其他“隐藏”文件。这是一种安全
		 * 措施，以防止访问时区目录外的文件。
		 */
		if (fc_direntry->d_name[0] == '.')
			continue;

		if (strlen(fc_direntry->d_name) == fc_fnamelen &&
			pg_strncasecmp(fc_direntry->d_name, fc_fname, fc_fnamelen) == 0)
		{
			/* 找到了匹配项 */
			strlcpy(fc_canonname, fc_direntry->d_name, fc_canonnamelen);
			fc_found = true;
			break;
		}
	}

	FreeDir(fc_dirdesc);

	return fc_found;
}


/*
 * 我们在哈希表中保留加载的时区，以便不必每次选择时区时都加载
 * 和解析 TZ 定义文件。因为我们希望时区名称以大小写不敏感的方式找到，
 * 所以哈希键是该时区的全大写名称。
 */
typedef struct
{
	/* tznameupper 包含时区的全大写名称 */
	char		tznameupper[TZ_STRLEN_MAX + 1];
	pg_tz		tz;
} pg_tz_cache;

static HTAB *timezone_cache = NULL;


static bool fc_init_timezone_hashtable(void)
{
	HASHCTL		fc_hash_ctl;

	fc_hash_ctl.keysize = TZ_STRLEN_MAX + 1;
	fc_hash_ctl.entrysize = sizeof(pg_tz_cache);

	timezone_cache = hash_create("Timezones",
								 4,
								 &fc_hash_ctl,
								 HASH_ELEM | HASH_STRINGS);
	if (!timezone_cache)
		return false;

	return true;
}

/*
 * 从文件或缓存中加载时区。
 * 不验证时区是否可接受！
 *
 * “GMT”始终被解释为 tzparse() 定义，未尝试从文件系统加载定义。这有许多好处：
 * 1. 它保证成功，因此我们不会遇到引导默认时区设置不能工作的失败模式（如
 * 操作系统尝试提供一个意识到闰秒的“GMT”版本时可能发生的情况）。
 * 2. 因为我们没有访问文件系统，所以可以安全地在 my_exec_path 未知
 * 时初始化“GMT”区域定义。
 * 3. 它足够快，因此在后续从 postgresql.conf 覆盖引导默认时区设置时，
 * 我们不会浪费太多时间。
 */
pg_tz * pg_tzset(const char *fc_name)
{
	pg_tz_cache *fc_tzp;
	struct state fc_tzstate;
	char		fc_uppername[TZ_STRLEN_MAX + 1];
	char		fc_canonname[TZ_STRLEN_MAX + 1];
	char	   *fc_p;

	if (strlen(fc_name) > TZ_STRLEN_MAX)
		return NULL;			/* 不会适配 */

	if (!timezone_cache)
		if (!fc_init_timezone_hashtable())
			return NULL;

	/*
	 * 将给定名称转换为大写以进行不区分大小写的哈希表搜索。
	 * (我们可以选择小写，但我们更喜欢大写，以便在名称是
	 * POSIX 风格的时区规范的情况下，从 tzparse() 获取一致的大写结果。)
	 */
	fc_p = fc_uppername;
	while (*fc_name)
		*fc_p++ = pg_toupper((unsigned char) *fc_name++);
	*fc_p = '\0';

	fc_tzp = (pg_tz_cache *) hash_search(timezone_cache,
									  fc_uppername,
									  HASH_FIND,
									  NULL);
	if (fc_tzp)
	{
		/* 时区在缓存中找到，不需要更多操作 */
		return &fc_tzp->tz;
	}

	/*
	 * "GMT" 始终发送到 tzparse()，以上讨论一致。
	 */
	if (strcmp(fc_uppername, "GMT") == 0)
	{
		if (!tzparse(fc_uppername, &fc_tzstate, true))
		{
			/* 这真的，真的不应该发生... */
			elog(ERROR, "could not initialize GMT time zone");
		}
		/* 使用大写名称作为规范名称 */
		strcpy(fc_canonname, fc_uppername);
	}
	else if (tzload(fc_uppername, fc_canonname, &fc_tzstate, true) != 0)
	{
		if (fc_uppername[0] == ':' || !tzparse(fc_uppername, &fc_tzstate, false))
		{
			/* 未知时区。失败我们的调用而不是加载 GMT！ */
			return NULL;
		}
		/* 对于 POSIX 时区规范，使用大写名称作为规范名称 */
		strcpy(fc_canonname, fc_uppername);
	}

	/* 将时区保存在缓存中 */
	fc_tzp = (pg_tz_cache *) hash_search(timezone_cache,
									  fc_uppername,
									  HASH_ENTER,
									  NULL);

	/* hash_search 已经将 uppername 复制到哈希键中 */
	strcpy(fc_tzp->tz.TZname, fc_canonname);
	memcpy(&fc_tzp->tz.state, &fc_tzstate, sizeof(fc_tzstate));

	return &fc_tzp->tz;
}

/*
 * 加载固定 GMT 偏移的时区。
 * 这用于 SQL 规范 SET TIME ZONE INTERVAL 'foo' 的情况。
 * 它在其他方面等同于 pg_tzset()。
 *
 * GMT 偏移以秒为单位指定，正值表示位于格林威治以西
 * （即，POSIX 而不是 ISO 符号约定）。但是，我们在可显示的
 * 缩写中使用 ISO 符号约定。
 *
 * 注意：如果指定的偏移超出了 zic 库允许的范围，
 * 这可能会失败（返回 NULL）。
 */
pg_tz * pg_tzset_offset(long fc_gmtoffset)
{
	long		fc_absoffset = (fc_gmtoffset < 0) ? -fc_gmtoffset : fc_gmtoffset;
	char		fc_offsetstr[64];
	char		fc_tzname[128];

	snprintf(fc_offsetstr, sizeof(fc_offsetstr),
			 "%02ld", fc_absoffset / SECS_PER_HOUR);
	fc_absoffset %= SECS_PER_HOUR;
	if (fc_absoffset != 0)
	{
		snprintf(fc_offsetstr + strlen(fc_offsetstr),
				 sizeof(fc_offsetstr) - strlen(fc_offsetstr),
				 ":%02ld", fc_absoffset / SECS_PER_MINUTE);
		fc_absoffset %= SECS_PER_MINUTE;
		if (fc_absoffset != 0)
			snprintf(fc_offsetstr + strlen(fc_offsetstr),
					 sizeof(fc_offsetstr) - strlen(fc_offsetstr),
					 ":%02ld", fc_absoffset);
	}
	if (fc_gmtoffset > 0)
		snprintf(fc_tzname, sizeof(fc_tzname), "<-%s>+%s",
				 fc_offsetstr, fc_offsetstr);
	else
		snprintf(fc_tzname, sizeof(fc_tzname), "<+%s>-%s",
				 fc_offsetstr, fc_offsetstr);

	return pg_tzset(fc_tzname);
}


/*
 * 初始化时区库
 *
 * 在 GUC 变量初始化开始之前调用。其目的是
 * 确保 log_timezone 在任何日志 GUC
 * 变量可能设置为需要 elog.c 提供
 * 时间戳的值（例如，log_line_prefix）之前具有有效值。
 * 我们也可以将 session_timezone 初始化为一些有效的值。
 */
void pg_timezone_initialize(void)
{
	/*
	 * 我们可能还不知道 PGSHAREDIR 在哪里（特别是在
	 * EXEC_BACKEND 子进程中）。因此使用 "GMT"，pg_tzset 强制它
	 * 不参考文件系统进行解释。这与 guc.c 中这些变量的引导默认值相对应，
	 * 尽管原则上它可能是不同的。
	 */
	session_timezone = pg_tzset("GMT");
	log_timezone = session_timezone;
}


/*
 * 列举可用时区的函数
 *
 * 请注意，pg_tzenumerate_next() 将返回指向 pg_tzenum
 * 结构的指针，因此数据仅在下一次调用之前有效。
 *
 * 所有数据在当前上下文中使用 palloc 分配。
 */
#define MAX_TZDIR_DEPTH 10

struct pg_tzenum
{
	int			baselen;
	int			depth;
	DIR		   *dirdesc[MAX_TZDIR_DEPTH];
	char	   *dirname[MAX_TZDIR_DEPTH];
	struct pg_tz tz;
};

/* typedef pg_tzenum 在 pgtime.h 中声明 */

pg_tzenum *
pg_tzenumerate_start(void)
{
	pg_tzenum  *fc_ret = (pg_tzenum *) palloc0(sizeof(pg_tzenum));
	char	   *fc_startdir = pstrdup(fc_pg_TZDIR());

	fc_ret->baselen = strlen(fc_startdir) + 1;
	fc_ret->depth = 0;
	fc_ret->dirname[0] = fc_startdir;
	fc_ret->dirdesc[0] = AllocateDir(fc_startdir);
	if (!fc_ret->dirdesc[0])
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open directory \"%s\": %m", fc_startdir)));
	return fc_ret;
}

void pg_tzenumerate_end(pg_tzenum *fc_dir)
{
	while (fc_dir->depth >= 0)
	{
		FreeDir(fc_dir->dirdesc[fc_dir->depth]);
		pfree(fc_dir->dirname[fc_dir->depth]);
		fc_dir->depth--;
	}
	pfree(fc_dir);
}

pg_tz * pg_tzenumerate_next(pg_tzenum *fc_dir)
{
	while (fc_dir->depth >= 0)
	{
		struct dirent *fc_direntry;
		char		fc_fullname[MAXPGPATH * 2];
		struct stat fc_statbuf;

		fc_direntry = ReadDir(fc_dir->dirdesc[fc_dir->depth], fc_dir->dirname[fc_dir->depth]);

		if (!fc_direntry)
		{
			/* 该目录的结束 */
			FreeDir(fc_dir->dirdesc[fc_dir->depth]);
			pfree(fc_dir->dirname[fc_dir->depth]);
			fc_dir->depth--;
			continue;
		}

		if (fc_direntry->d_name[0] == '.')
			continue;

		snprintf(fc_fullname, sizeof(fc_fullname), "%s/%s",
				 fc_dir->dirname[fc_dir->depth], fc_direntry->d_name);
		if (stat(fc_fullname, &fc_statbuf) != 0)
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not stat \"%s\": %m", fc_fullname)));

		if (S_ISDIR(fc_statbuf.st_mode))
		{
			/* 进入子目录 */
			if (fc_dir->depth >= MAX_TZDIR_DEPTH - 1)
				ereport(ERROR,
						(errmsg_internal("timezone directory stack overflow")));
			fc_dir->depth++;
			fc_dir->dirname[fc_dir->depth] = pstrdup(fc_fullname);
			fc_dir->dirdesc[fc_dir->depth] = AllocateDir(fc_fullname);
			if (!fc_dir->dirdesc[fc_dir->depth])
				ereport(ERROR,
						(errcode_for_file_access(),
						 errmsg("could not open directory \"%s\": %m",
								fc_fullname)));

			/* 从新目录重新开始读取 */
			continue;
		}

		/*
		 * 使用 tzload() 加载此时区而不是 pg_tzset()，因此我们不填充
		 * 缓存。同样，不要求规范拼写：我们已经
		 * 知道它，而 pg_open_tzfile 的发现方式相当
		 * 低效。
		 */
		if (tzload(fc_fullname + fc_dir->baselen, NULL, &fc_dir->tz.state, true) != 0)
		{
			/* 时区无法加载，忽略它 */
			continue;
		}

		if (!pg_tz_acceptable(&fc_dir->tz))
		{
			/* 忽略闰秒时区 */
			continue;
		}

		/* 好的，返回规范的时区名称拼写。 */
		strlcpy(fc_dir->tz.TZname, fc_fullname + fc_dir->baselen,
				sizeof(fc_dir->tz.TZname));

		/* 时区加载成功。 */
		return &fc_dir->tz;
	}

	/* 没有找到更多内容 */
	return NULL;
}
